package fr.vahren.dotastrat.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import fr.vahren.dotastrat.game.D2SGame;
import fr.vahren.dotastrat.game.D2SGameState;
import fr.vahren.dotastrat.game.D2SMessageType;
import fr.vahren.dotastrat.game.effect.D2SAbilityListener;
import fr.vahren.dotastrat.game.effect.D2SAttackListner;
import fr.vahren.dotastrat.game.effect.D2SEffect;
import fr.vahren.dotastrat.game.effect.D2SMoveListener;

public class D2SHeroUnit extends D2SUnit {

	private static final int[] levelUpExp = { 0, 200, 500, 900, 1400, 2000,
			2600, 3200, 4400, 5400, 6000, 8200, 9000, 10400, 11900, 13500,
			15200, 17000, 18900, 20900, 23000, 25200, 27500, 29900, 32400 };

	public static final int[][] expTable = {
			{ 240, 280, 340, 420, 520, 640, 760, 880, 1000, 1120, 1240, 1360,
					1480, 1600, 1720, 1840, 1960, 2080, 2200, 2320, 2440, 2560,
					2680, 2800, 2920 },
			{ 155, 180, 215, 260, 315, 380, 445, 510, 575, 640, 705, 770, 835,
					900, 965, 1030, 1095, 1160, 1225, 1290, 1355, 1420, 1485,
					1550, 1615 },
			{ 70, 84, 104, 131, 165, 205, 245, 286, 326, 366, 407, 447, 487,
					528, 568, 608, 649, 689, 729, 770, 810, 850, 891, 931, 971 },
			{ 50, 60, 75, 95, 120, 150, 180, 210, 240, 270, 300, 330, 360, 390,
					420, 450, 480, 510, 540, 570, 600, 630, 660, 690, 720 },
			{ 39, 47, 59, 75, 95, 119, 143, 167, 191, 215, 239, 263, 287, 311,
					335, 359, 383, 407, 431, 455, 479, 503, 527, 551, 575 } };

	private List<D2SAbility> abilities = new ArrayList<>();

	private List<D2SItem> items = new ArrayList<>();

	private List<D2SEffect> effects = new ArrayList<>();

	private List<D2SAttackListner> atkListeners = new ArrayList<>();
	private List<D2SMoveListener> moveListeners = new ArrayList<>();
	private List<D2SAbilityListener> abilityListeners = new ArrayList<>();

	private D2SHeroStats stats;

	private String name;

	private int id;

	private D2SUnitClass unitClass;

	private boolean isIllusion;

	private boolean invisible;

	private boolean hasChosenAction;

	private boolean moving;

	private boolean hasAttackedHero;

	private boolean melee;

	private int killStreak = 0;

	private int reliableGold = 0;
	private int unreliableGold = 0;

	private D2SGame game;

	/**
	 * Used to level up abilities/stats
	 */
	private int nonUsedSkillPoints = 1;

	private int respawnCooldown;

	public D2SHeroUnit(String name, D2SUnitClass unitClass, D2SHeroStats stats,
			D2SGame g) {
		id = D2SGame.id();
		this.unitClass = unitClass;
		this.name = name;
		this.stats = stats;
		game = g;
	}

	public D2SHeroUnit(String name, D2SUnitClass unitClass, int id, D2SGame g) {
		this.id = id;
		this.unitClass = unitClass;
		this.name = name;
		stats = new D2SHeroStats();
		game = g;
	}

	@SuppressWarnings("unchecked")
	public JSONObject generateInfo(D2SGameState phase) {
		JSONObject unit = new JSONObject();

		JSONArray abilities = new JSONArray();
		for (D2SAbility abil : this.abilities) {
			abilities.add(abil.generateInfo());
		}
		unit.put("abilities", abilities);

		JSONArray items = new JSONArray();
		for (D2SItem item : this.items) {
			items.add(item.generateInfo());
		}
		unit.put("items", items);

		unit.put("hasChosenAction", hasChosenAction);
		unit.put("class", unitClass.toString());
		unit.put("isIllusion", isIllusion);
		unit.put("id", id);
		unit.put("name", name);
		unit.put("skillPoints", nonUsedSkillPoints);
		unit.put("reliableGold", reliableGold);
		unit.put("unreliableGold", unreliableGold);

		if (phase == D2SGameState.CHOOSE_ACTION) {
			unit.put("possibleActions", generatePossibleActions());
		}

		// stats
		unit.put("stats", stats.generateInfo());

		return unit;
	}

	@SuppressWarnings("unchecked")
	private JSONArray generatePossibleActions() {
		JSONArray list = new JSONArray();
		// can attack?
		if (canAttack()) {
			list.add("atk");
		}
		if (canMove()) {
			list.add("move");
		}
		for (D2SAbility a : abilities) {
			if (canUse(a)) {
				list.add("abil" + a.getId());
			}
		}
		for (D2SItem i : items) {
			if (canUse(i)) {
				list.add("item" + i.getId());
			}
		}

		return list;
	}

	private boolean canUse(D2SAbility a) {
		return !a.isOnCooldown() && hasEnoughMana(a.getManaCost());
	}

	private boolean hasEnoughMana(int manaCost) {
		return stats.getMana() >= manaCost;
	}

	private boolean canMove() {
		// TODO Auto-generated method stub
		return true;
	}

	private boolean canAttack() {
		// TODO Auto-generated method stub
		return true;
	}

	/**
	 * @return the abilities
	 */
	public List<D2SAbility> getAbilities() {
		return abilities;
	}

	/**
	 * @param abilities
	 *            the abilities to set
	 */
	public void setAbilities(List<D2SAbility> abilities) {
		this.abilities = abilities;
	}

	/**
	 * @return the items
	 */
	public List<D2SItem> getItems() {
		return items;
	}

	/**
	 * @param items
	 *            the items to set
	 */
	public void setItems(List<D2SItem> items) {
		this.items = items;
	}

	/**
	 * @return the name
	 */
	public String getName() {
		return name;
	}

	/**
	 * @param name
	 *            the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * @return the id
	 */
	public int getId() {
		return id;
	}

	/**
	 * @param id
	 *            the id to set
	 */
	public void setId(int id) {
		this.id = id;
	}

	/**
	 * @return the unitClass
	 */
	public D2SUnitClass getUnitClass() {
		return unitClass;
	}

	/**
	 * @param unitClass
	 *            the unitClass to set
	 */
	public void setUnitClass(D2SUnitClass unitClass) {
		this.unitClass = unitClass;
	}

	/**
	 * @return the isIllusion
	 */
	public boolean isIllusion() {
		return isIllusion;
	}

	/**
	 * @param isIllusion
	 *            the isIllusion to set
	 */
	public void setIllusion(boolean isIllusion) {
		this.isIllusion = isIllusion;
	}

	/**
	 * @return the hasChosenAction
	 */
	public boolean isHasChosenAction() {
		return hasChosenAction;
	}

	/**
	 * @param hasChosenAction
	 *            the hasChosenAction to set
	 */
	public void setHasChosenAction(boolean hasChosenAction) {
		this.hasChosenAction = hasChosenAction;
	}

	public D2SItem getItem(int id) {
		for (D2SItem item : items) {
			if (item.getId() == id) {
				return item;
			}
		}
		return null;
	}

	public D2SAbility getAbility(int id) {
		for (D2SAbility ability : abilities) {
			if (ability.getId() == id) {
				return ability;
			}
		}
		return null;
	}

	/**
	 * @return the stats
	 */
	public D2SHeroStats getStats() {
		return stats;
	}

	/**
	 * @param stats
	 *            the stats to set
	 */
	public void setStats(D2SHeroStats stats) {
		this.stats = stats;
	}

	public D2SHeroUnit snapshot() {
		D2SHeroUnit copy = new D2SHeroUnit(name, unitClass, id, game);
		copy.setIllusion(isIllusion);
		copy.setStats(stats.snapshot());
		return copy;
	}

	/**
	 * @return the moving
	 */
	public boolean isMoving() {
		return moving;
	}

	/**
	 * @param moving
	 *            the moving to set
	 */
	public void setMoving(boolean moving) {
		this.moving = moving;
	}

	public int getSpeed() {
		return stats.getSpeed();
	}

	/**
	 * @return the melee
	 */
	public boolean isMelee() {
		return melee;
	}

	/**
	 * @param melee
	 *            the melee to set
	 */
	public void setMelee(boolean melee) {
		this.melee = melee;
	}

	public void applyAttackDamage(D2SUnit target, boolean reduced, D2SUnit unit) {
		// trigger on Attack effects

		int baseDmg = getDamage();
		int armor = target.getArmor();
		int dmg = reduceDamage(baseDmg, armor) / (reduced ? 2 : 1);

		target.removeHealth(dmg);

		for (D2SAttackListner l : atkListeners) {
			l.onAttack(target, unit);
		}
		if (isHero()) {
			String message = unit.getName() + " deals " + dmg + " damage to "
					+ target.getName();
			game.sendToAnyoneThatCanSee(target.getId(), Arrays.asList(game
					.getMap().getZoneFromHero(target.getId())), game
					.getPlayerFromUnit(unit.getId()).getSide(), message,
					D2SMessageType.ATTACK);
		}

	}

	public int getDamage() {
		return stats.getDamage();
	}

	public int getArmor() {
		return stats.getArmor();
	}

	public void removeHealth(int dmg) {
		stats.setHealth(stats.getHealth() - dmg);
	}

	private int reduceDamage(int baseDmg, int armor) {
		if (armor <= -2) {
			return (int) Math.floor((1 + D2SGame.log2(-armor) / 10) * baseDmg);
		} else if (armor >= 2) {
			return (int) Math.floor((1 - D2SGame.log2(armor) / 10) * baseDmg);
		} else {
			return (int) Math.floor((1 - armor * .05) * baseDmg);
		}

	}

	public void heal() {
		stats.setHealth(stats.getMaxHealth());
		stats.setMana(stats.getMaxMana());
	}

	public boolean hasTrueSightOn() {
		// TODO Auto-generated method stub
		return false;
	}

	public boolean isInvisible() {
		return invisible;
	}

	public void setInvisible(boolean invisible) {
		this.invisible = invisible;
	}

	public void regen() {
		if (!isDead()) {
			heal(stats.getHealthRegen(), stats.getManaRegen());
		}
	}

	public void heal(int hp, int mp) {
		stats.setHealth(Math.min(stats.getMaxHealth(), stats.getHealth() + hp));
		stats.setMana(Math.min(stats.getMana() + mp, stats.getMaxMana()));
	}

	/**
	 * Applies effects on dying (gold loss and respawn cooldown).
	 */
	public void die() {
		if (isHero()) {
			// remove unereliable gold
			int loss = Math.min(unreliableGold, 30 * stats.getLevel());
			game.sendMessageTo(game.getPlayerFromUnit(id).getId(), name
					+ " loses " + loss + " unreliable gold",
					D2SMessageType.DEATH);
			unreliableGold -= loss;

			// TODO remove debuffs and buffs

			// respawning cooldown
			respawnCooldown = getRespawnTime(stats.getLevel());
		}
	}

	private int getRespawnTime(int level) {
		return (int) (level * 0.2 + 1);
	}

	public void setAttackers(List<Integer> attackers) {
		this.attackers = attackers;
	}

	public int getKillStreak() {
		return killStreak;
	}

	public void setKillStreak(int killStreak) {
		this.killStreak = killStreak;
	}

	/**
	 * Gain exp and then chec for level up.
	 * 
	 * @param exp
	 */
	public void exp(int exp) {
		stats.gainExp(exp);

		// check for level up
		int levelGain = getLevel(stats.getExp()) - stats.getLevel();
		if (levelGain > 0) {
			stats.gainLevel(levelGain);
			game.sendMessageTo(game.getPlayerFromUnit(id).getId(), name
					+ " has leveled up to level" + stats.getLevel(),
					D2SMessageType.LEVELUP);
			nonUsedSkillPoints += levelGain;
		}
	}

	private int getLevel(int exp) {
		for (int i = 0; i < levelUpExp.length; i++) {
			if (exp > levelUpExp[i]) {
				return i + 1;
			}
		}
		return levelUpExp.length;
	}

	public void earn(int gold, boolean reliable) {
		if (reliable) {
			reliableGold += gold;
		} else {
			unreliableGold += gold;
		}
	}

	public boolean isHero() {
		return !isIllusion && unitClass != D2SUnitClass.ANCIENT
				&& unitClass != D2SUnitClass.TOWER
				&& unitClass != D2SUnitClass.BARRACKS;
	}

	public int getReliableGold() {
		return reliableGold;
	}

	public void setReliableGold(int reliableGold) {
		this.reliableGold = reliableGold;
	}

	public int getUnreliableGold() {
		return unreliableGold;
	}

	public void setUnreliableGold(int unreliableGold) {
		this.unreliableGold = unreliableGold;
	}

	public int getNonUsedSkillPoints() {
		return nonUsedSkillPoints;
	}

	public void setNonUsedSkillPoints(int nonUsedSkillPoints) {
		this.nonUsedSkillPoints = nonUsedSkillPoints;
	}

	public int getRespawnCooldown() {
		return respawnCooldown;
	}

	public void setRespawnCooldown(int respawnCooldown) {
		this.respawnCooldown = respawnCooldown;
	}

	public void tickAllCooldowns() {
		if (respawnCooldown > 0) {
			respawnCooldown--;
		}
	}

	public boolean isDead() {
		return stats.getHealth() == 0;
	}

	/**
	 * @return the effects
	 */
	public List<D2SEffect> getEffects() {
		return effects;
	}

	/**
	 * @param effects
	 *            the effects to set
	 */
	public void setEffects(List<D2SEffect> effects) {
		this.effects = effects;
	}

	public void removeEffect(D2SEffect effect) {
		effects.remove(effect);
	}

	/**
	 * @return the hasAttackedHero
	 */
	public boolean isHasAttackedHero() {
		return hasAttackedHero;
	}

	/**
	 * @param hasAttackedHero
	 *            the hasAttackedHero to set
	 */
	public void setHasAttackedHero(boolean hasAttackedHero) {
		this.hasAttackedHero = hasAttackedHero;
	}

	@Override
	public String toString() {
		return getName();
	}

	@Override
	public D2SSide getSide() {
		return game.getPlayerFromUnit(id).getSide();
	}

	/**
	 * @return the atkListeners
	 */
	public List<D2SAttackListner> getAtkListeners() {
		return atkListeners;
	}

	/**
	 * @param atkListeners
	 *            the atkListeners to set
	 */
	public void setAtkListeners(List<D2SAttackListner> atkListeners) {
		this.atkListeners = atkListeners;
	}

	/**
	 * @return the moveListeners
	 */
	public List<D2SMoveListener> getMoveListeners() {
		return moveListeners;
	}

	/**
	 * @param moveListeners
	 *            the moveListeners to set
	 */
	public void setMoveListeners(List<D2SMoveListener> moveListeners) {
		this.moveListeners = moveListeners;
	}

	/**
	 * @return the abilityListeners
	 */
	public List<D2SAbilityListener> getAbilityListeners() {
		return abilityListeners;
	}

	/**
	 * @param abilityListeners
	 *            the abilityListeners to set
	 */
	public void setAbilityListeners(List<D2SAbilityListener> abilityListeners) {
		this.abilityListeners = abilityListeners;
	}

	public void registerAtkListener(D2SAttackListner l) {
		atkListeners.add(l);
	}

	public void removeAtkListener(D2SAttackListner l) {
		atkListeners.remove(l);
	}

	public void registerMoveListener(D2SMoveListener l) {
		moveListeners.add(l);
	}

	public void removeMoveListener(D2SMoveListener l) {
		moveListeners.remove(l);
	}

	public void registerAbilityListener(D2SAbilityListener l) {
		abilityListeners.add(l);
	}

	public void removeAbilityListener(D2SAbilityListener l) {
		abilityListeners.remove(l);
	}

	public void triggerMoveListener(int startZone, int targetZone) {
		for (D2SMoveListener l : moveListeners) {
			l.onMove(this, startZone, targetZone);
		}
	}

	public void triggerAbilityListeners(D2SAbility ability) {
		for (D2SAbilityListener l : abilityListeners) {
			l.onAbilityUsed(this, ability);
		}
	}

	@Override
	public int getCurrentHealth() {
		return stats.getHealth();
	}

	@Override
	public int getMaxHealth() {
		return stats.getMaxHealth();
	}

	@Override
	public int getCurrentMana() {
		return stats.getMana();
	}

	@Override
	public int getMaxMana() {
		return stats.getMaxMana();
	}

	@Override
	public int getAttackRange() {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public int getGoldBounty(int nbAttackers) {
		return (getKillStreakBonus(killStreak) + 200 + stats.getLevel() * 9)/nbAttackers;
	}

	private int getKillStreakBonus(int killStreak) {
		if (killStreak < 3) {
			return 0;
		} else if (killStreak < 10) {
			return (killStreak - 2) * 125;
		} else {
			return 1000;
		}
	}

	@Override
	public int getExpBounty(int nbAttackers) {
		return expTable[nbAttackers - 1][stats.getLevel() - 1];
	}
}
